Repository navigation
fix(databricks): a schema the contract stops using is destroyed, not left failing the apply - #873
Merged
Merged
Conversation
…unction, view or volume Unity Catalog deletes a schema only when it holds nothing: the 2026-11-03 Free Edition run measured that `tofu destroy` fails at a schema that still holds functions. The stand-in refused only for tables, so a schema holding a mask function or a seeded metric view was deleted, and a test could pass where a workspace fails.
…left failing the apply
When a table moves to another schema, or a schema's last expose is removed, the plan
destroys the old schema, and Unity Catalog refuses to delete a schema that still holds
anything. The product's metric views and its column mask and row filter functions there
are no OpenTofu resource, so `fluid apply` failed at that destroy ("Schema is not empty")
until someone dropped them by hand.
The schemas now come from the saved plan: each databricks_schema it only deletes that the
contract no longer names, so only schemas this environment's state holds are looked in.
- Before `tofu apply` (new plugin hook prepare_destroys), drop this product's metric views
(fluid.product_id, owned by the deploy identity) whose table the plan destroys or is
gone, and fluid_-shaped functions the deploy identity owns that no table of the schema
uses. A function a table still uses is never dropped while the table exists: Unity
Catalog names a mask's function in the table, so a gap would let anyone with CREATE
FUNCTION on the schema plant one under that name.
- OpenTofu destroys a table before its schema inside one apply, so such a schema still
fails there. The engine then calls unblock_destroys, which drops the functions the
destroyed tables used (no table uses them now), plans again, and applies only if that
plan is nothing but those schema deletes (fail closed otherwise).
- --mode dry-run prints the drops and the functions that wait, and runs none.
Engine plumbing: runner.planned_deletes and runner.errored_resources (one event
iterator shared with planned_removals), _apply_saved_plan and _applied out of
_apply_via_opentofu, and one _run_hook for the optional plugin hooks, whose typed
refusals map through _binding_error (now taking the event).
Prior art: none for clearing objects outside state between a child's and a parent's
destroy. force_destroy would delete data not in state; OpenTofu's -exclude needs 1.9
(the floor is 1.6.0); a destroy-time provisioner does not run once its block is removed
(hashicorp/terraform#13549).
…unts as bound The Tables API names a row filter's or a mask's function in full, and the rules that clear a schema the plan destroys read it that way. A shorter name is now read against the table's own catalog and schema, so no form of it can make a function a table still uses look unused and be dropped while the table exists (raised by the branch's security review as the one assumption that would change its verdict). The resume's lead line says why the schema waited, and HONESTLY_TESTED counts the tests the branch now has (10 + 23 + 2).
…d resumes only what it cleared From the branch's defect scan and inspection: - A metric view definition PyYAML cannot load (an impossible date is a ValueError, not a YAMLError) or a Tables API row of an unexpected shape no longer escapes prepare_destroys as a traceback before tofu apply: the definition names no table, the row is read defensively, and anything else that breaks while one schema is read leaves that schema as it is, with a note. - The product id comes from the contract (databricks_metrics.product_id_of, shared with compile_metric_views), so a contract with no Databricks expose left, which compiles no metric views, still names its views. - unblock_destroys returns the plan addresses it cleared, and the engine resumes only when they are every resource the apply failed on: a delete that failed for another reason is no longer retried behind the cleared ones. - The schema is read through table-summaries as well as the Tables list, so a metric view is found whichever listing Unity Catalog gives it in. - The dry-run heading fits a schema holding only objects this product did not make; iac/base.py lists the two new optional capabilities; HONESTLY_TESTED counts the tests (13 + 24 + 2) and, from the #865 docs check, the metric-view row's 5 stand-in tests and that CREATE OR REPLACE VIEW makes the replacer the view's owner.
…ck to the Tables list From the re-check of the previous commit: a "not found" from table-summaries alone made a schema that exists look gone, so it was skipped with no note. Only the Tables list or the functions list answering "not found" now means the schema is gone; table-summaries answering it leaves the Tables list to name what the schema holds. Also reflows the stand-in's docstring line the rebase merged past 100 columns.
📄 Documentation ReminderThis PR appears to be missing a documentation reference. Our docs live in a separate repo. Please update the PR description with one of:
See the Contributing Guide for details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A Databricks table that moves to another schema, or a schema whose last expose is removed, no longer leaves
fluid applyfailing at OpenTofu's destroy of the old schema ("Schema is not empty"). The product's metric views and its column-mask and row-filter functions there are no OpenTofu resource, and Unity Catalog refuses to delete a schema that holds anything, so until now someone had to drop them by hand. This was the documented "known limit" of #865 and of the masks' functions.How
databricks_schemathe plan only deletes and the contract no longer names (databricks_vacated.vacated). Only schemas this environment's own OpenTofu state holds are looked in, so another environment of the product (an overlay that changes the schema, same catalog, same identity) keeps its objects.tofu apply(new plugin hookprepare_destroys, afterpre_apply): drop this product's metric views (fluid.product_id, owned by the deploy identity) whose table the plan destroys or is gone, andfluid_rf_*/fluid_mask_*functions the deploy identity owns that no table of the schema uses.SET MASK <function name>), so dropping it early would leave the table reading through whatever function someone withCREATE FUNCTIONon the schema then made under that name.unblock_destroyshook: the tables are gone, so their functions are dropped by the same rules, and a second plan is applied only if it holds nothing but those schema deletes (the data-loss gate already passed them). Anything else (an error a failed delete does not account for, a plugin that clears nothing, a second plan with any other change) leaves the apply's own failure as before.--mode dry-runprints theDROP VIEWbefore the apply and theDROP FUNCTIONs that wait for the tables, and runs none.Engine plumbing:
runner.planned_deletes/runner.errored_resources(sharing one event iterator withplanned_removals),_apply_saved_plan/_appliedout of_apply_via_opentofu, and one_run_hookfor the optional plugin hooks.Borrow before build
No prior art for clearing objects outside state between a child's and its parent's destroy.
force_destroywould delete data not in state; OpenTofu-excludeneeds 1.9 (the floor is 1.6.0); a destroy-time provisioner never runs once its resource block is removed (hashicorp/terraform#13549); Databricks Asset Bundles have no such hook (databricks/cli#1848). Reused the repo's own seams:_schema_views/_drop_stalechecks,drop_orphan_functions' name shapes and owner check, the engine's optional-hook pattern.Validation
fluidCLI,tofu1.13 and databricks/databricks 1.140.0 against the in-process Unity Catalog stand-in): a table with a metric view, a mask and a row filter moves frommain.salestomain.other.--mode dry-runprints theDROP VIEWbefore the apply and theDROP FUNCTIONs after the table's destroy, and runs none; the apply exits 0, printstofu plan (the destroys that waited): +0 ~0 -1andtofu apply complete: +11 ~0 -11, andmain.salesis gone with nothing left in it. Removing a schema's last expose does the same.tests/iac/test_iac_databricks_vacated.py(14, the rules against the stand-in),tests/iac/test_iac_apply_engine_destroys.py(24, the engine withtofustubbed), 2 fake e2e intests/iac/test_iac_databricks_metrics_fake_e2e.py(in the existing Stage 1 Databricks lane).mypy --stricton the new module.tests/iac+tests/cli(non-integration): 7121 passed, 2 failed intests/cli/test_lazy_core_commands.py, the known shared-venv baseline that fails on any tree. All Databricks fake e2e tiers serially: 61 passed, 18 skipped (need databricks-sdk), 2 failed with the stand-in'swrite: broken pipeand passed on re-run. That flake is the stand-in's (it answers HTTP/1.0 and closes every connection): in an interleaved A/B of the data quality tier at equal load,origin/mainfailed 2 of 4 runs and this branch 1 of 4. Full suite once: 35,691 passed; the 32 failures and 2 errors are the known shared-venv baselines (moto tiers needing a provider download, dotenv_dlt, duckdb sandbox, lazy-core imports, compat gate,bashlexcollection) plus 3 stand-in transport flakes under-n 6.ripwire --quality-delta(branch diff on a freshorigin/maintree): the rows on touched code areDatabricksIacPlugin+25 lines (the two hooks), short-horizon churn of the refactored hook runners, test-helper duplication, and a dead-code false positive (MetricViews.plan_actionsis called atdatabricks.py:2933). Refactored during the branch: one_run_hookfor the optional plugin hooks,_apply_saved_plan/_appliedout of_apply_via_opentofu, the plan parsing inrunner.Reviews
149043f3).fd5f6644. (1) A metric view definition PyYAML cannot load (an impossible date is aValueError), or a row of an odd shape, escaped as a traceback beforetofu apply. (2) The product id came from the compiled views, which are empty when no Databricks expose is left. Refuted but hardened anyway: the resume now runs only for the destroys the plugin cleared (it returns their addresses), and metric views are also found throughtable-summaries.Commits: the stand-in fidelity fix, the change, then three review fixes (each its own commit). Rebased on
origin/main1e20c739.Not done here